/*
Copyright (C) 1997-2008 ZSNES Team ( zsKnight, _Demo_, pagefault, Nach )

http://www.zsnes.com
http://sourceforge.net/projects/zsnes
https://zsnes.bountysource.com

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
version 2 as published by the Free Software Foundation.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/

#include "../gblhdr.h"

#include <sys/param.h>
#include <sys/wait.h>
#include <signal.h>
#include <paths.h>
#include <grp.h>
#include <pwd.h>

#ifndef OPEN_MAX
#define OPEN_MAX 256
#endif

#include "safelib.h"

#include "../argv.h"

//Introducing secure forking ;) -Nach

#ifdef __linux__
static int z_setreuid(uid_t uid)
{
  return(setreuid(uid, uid));
}

static int z_setregid(gid_t gid)
{
  return(setregid(gid, gid));
}
#else
static int z_setreuid(uid_t uid)
{
  seteuid(uid);
  return(setuid(uid));
}

static int z_setregid(gid_t gid)
{
  setegid(gid);
  return(setgid(gid));
}
#endif


//Taken from the secure programming cookbook, somewhat modified
static bool spc_drop_privileges()
{
  gid_t newgid = getgid(), oldgid = getegid();
  uid_t newuid = getuid(), olduid = geteuid();

  char *name = getlogin();
  struct passwd *userinfo;

  if (name) { userinfo = getpwnam(name); }

  if (!olduid && !newuid && userinfo && userinfo->pw_uid) //If currently using su to root or sudo
  {
    newuid = userinfo->pw_uid;
  }

  if (!oldgid && !newgid && userinfo && userinfo->pw_gid) //Same thing, but now check the group
  {
    newgid = userinfo->pw_gid;
  }

  //If the above failed, we check for root via +s as a mode on the binary

  //If root privileges are to be dropped, be sure to pare down the ancillary
  //groups for the process before doing anything else because the setgroups()
  //system call requires root privileges.  Drop ancillary groups regardless of
  //whether privileges are being dropped temporarily or permanently.

  if (!olduid) { setgroups(1, &newgid); }

  if (((newgid != oldgid) && (z_setregid(newgid) == -1)) || ((newuid != olduid) && (z_setreuid(newuid) == -1)))
  {
    return(false);
  }

  //verify that the changes were successful
  if (newgid != oldgid && (setegid(oldgid) != -1 || getegid() != newgid)) { return(false); }
  if (newuid != olduid && (seteuid(olduid) != -1 || geteuid() != newuid)) { return(false); }

  return(true);
}

static int open_devnull(int fd)
{
  FILE *f = 0;

  if (!fd) { f = freopen(_PATH_DEVNULL, "rb", stdin); }
  else if (fd == 1) { f = freopen(_PATH_DEVNULL, "wb", stdout); }
  else if (fd == 2) { f = freopen(_PATH_DEVNULL, "wb", stderr); }
  return(f && fileno(f) == fd);
}

static bool array_contains(int *a, size_t size, int key)
{
  size_t i;
  for (i = 0; i < size; i++)
  {
    if (a[i] == key) { return(true); }
  }
  return(false);
}

static bool spc_sanitize_files(int *a, size_t size, int skip)
{
  int fd, fds;
  struct stat st;

  //Make sure all open descriptors other than the standard ones are closed
  if ((fds = getdtablesize()) == -1)
  {
    fds = OPEN_MAX;
  }
  for (fd = 3;  fd < fds;  fd++)
  {
    if ((fd != skip) && !array_contains(a, size, fd)) { close(fd); }
  }

  //Verify that the standard descriptors are open.  If they're not, attempt to
  //open them using /dev/null.  If any are unsuccessful, fail.
  for (fd = 0;  fd < 3;  fd++)
  {
    if (fstat(fd, &st) == -1 && (errno != EBADF || !open_devnull(fd)))
    {
      return(false);
    }
  }
  return(true);
}

//Pass array of file descriptors to leave open
pid_t safe_fork(int *a, size_t size)
{
  int filedes[2];
  if (!pipe(filedes))
  {
    char success = 0;
    pid_t childpid;
    if ((childpid = fork()) == -1) //Fork Failed
    {
      close(filedes[0]);
      close(filedes[1]);
      return(-1);
    }

    if (childpid) //Parent Process
    {
      close(filedes[1]); //Close writing
      read(filedes[0], &success, 1);
      close(filedes[0]);
      if (success)
      {
        return(childpid);
      }
      waitpid(childpid, 0, 0);
      return(-1);
    }


    //This is the child proccess

    close(filedes[0]); //Close reading

    if (!spc_sanitize_files(a, size, filedes[1]) || !spc_drop_privileges())
    {
      write(filedes[1], &success, 1);
      close(filedes[1]);
      _exit(0);
    }

    success = 1;
    write(filedes[1], &success, 1);
    close(filedes[1]);
    return(0);
  }
  return(-1);
}


//Introducing a popen which doesn't return until it knows for sure of program launched or couldn't open -Nach


//Forks, parent is paused until child successfully execs (returns child pid) or child exits (returns failure)
static pid_t parent_pause_fork()
{
  int filedes[2];
  if (!pipe(filedes))
  {
    int pid = fork();
    if (pid == -1) //Failed
    {
      close(filedes[0]);
      close(filedes[1]);
    }
    else if (pid > 0) //Parent
    {
      char success = 1;
      close(filedes[1]);
      read(filedes[0], &success, 1);
      close(filedes[0]);
      if (success)
      {
        return(pid);
      }
      waitpid(pid, 0, 0);
    }
    else //Child
    {
      close(filedes[0]);
      fcntl(filedes[1], F_SETFD, FD_CLOEXEC);
      return(-filedes[1]);
    }
  }
  return(0);
}

static void close_child(pid_t pid)
{
  char success = 0;
  write(-pid, &success, 1);
  close(-pid);
  _exit(0);
}

#define IS_PARENT(x) ((x) > 0)
#define IS_CHILD(x) ((x) < 0)
#define IS_FAIL(x) ((x) == 0)


static struct fp_pid_link
{
  FILE *fp;
  pid_t pid;
  struct fp_pid_link *next;
} fp_pids = { 0, 0, 0 };


FILE *safe_popen(char *command, const char *mode)
{
  //filedes[0] is for reading
  //filedes[1] is for writing.
  int filedes[2];

  if (mode && (*mode == 'r' || *mode == 'w') && !pipe(filedes))
  {
    pid_t childpid = parent_pause_fork();
    if (IS_PARENT(childpid))
    {
      FILE *fp;
      if (*mode == 'r')
      {
        close(filedes[1]);
        fp = fdopen(filedes[0], "r");
      }
      else
      {
        close(filedes[0]);
        fp = fdopen(filedes[1], "w");
      }

      if (fp)
      {
        struct fp_pid_link *link = &fp_pids;
        while (link->next)
        {
          link = link->next;
        }

        link->next = (struct fp_pid_link *)malloc(sizeof(struct fp_pid_link));
        if (link->next)
        {
          link->next->fp = fp;
          link->next->pid = childpid;
          link->next->next = 0;
          return(fp);
        }
        fclose(fp);
      }
      kill(childpid, SIGTERM);
      waitpid(childpid, 0, 0);
    }
    else if (IS_CHILD(childpid))
    {
      char **argv = build_argv(command);
      if (argv)
      {
        if (*mode == 'r')
        {
          dup2(filedes[1], STDOUT_FILENO);
        }
        else
        {
          dup2(filedes[0], STDIN_FILENO);
        }

        if (spc_sanitize_files(0, 0, -childpid) && spc_drop_privileges())
        {
          execvp(argv[0], argv);
        }
        free(argv);
      }
      close_child(childpid);
    }
    close(filedes[0]);
    close(filedes[1]);
  }
  return(0);
}

void safe_pclose(FILE *fp)
{
  struct fp_pid_link *link = &fp_pids;

  while (link->next && link->next->fp != fp)
  {
    link = link->next;
  }
  if (link->next->fp == fp)
  {
    struct fp_pid_link *dellink = link->next;
    fclose(fp);
    waitpid(link->next->pid, 0, 0);
    link->next = link->next->next;
    free(dellink);
  }
}
